package hugebunny;

import org.lwjgl.BufferUtils;
import org.lwjgl.LWJGLException;
import org.lwjgl.Sys;
import org.lwjgl.input.Keyboard;
import org.lwjgl.input.Mouse;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;
import org.lwjgl.opengl.GLContext;
import org.lwjgl.util.vector.Vector3f;
import org.newdawn.slick.Color;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.FloatBuffer;
import java.util.logging.Level;
import java.util.logging.Logger;

import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.util.glu.GLU.gluPerspective;

public class ModelDemo
{

	public static final boolean resizable = true;

	private static volatile boolean running = true;

	public static Vector3f position = new Vector3f(0, 0, 0);

	public static Vector3f rotation = new Vector3f(0, 0, 0);

	public static float[] lightPosition = { -2.19f, 1.36f, 11.45f, 1f };

	public static float zNear = 0.3f;

	public static final int gridSize = 10;

	public static final float tileSize = 0.20f;

	public static float zFar = 20f;

	public static float fogNear = 11f;

	public static float fogFar = 15f;

	public static Color fogColor = new Color(0f, 0f, 0f, 1f);

	public static final boolean fullscreen = false;

	public static int walkingSpeed = 10;

	public static int mouseSpeed = 2;

	public static final boolean vsync = true;

	public static boolean printFPS = false;

	public static final int maxLookUp = 85;

	public static final int maxLookDown = -85;

	public static final float ceilingHeight = 10;

	public static final float floorHeight = -1;

	public static int fov = 68;
	private static int fps;
	private static long lastFPS;
	private static long lastFrame;

	private static long getTime()
	{
		return (Sys.getTime() * 1000) / Sys.getTimerResolution();
	}

	private static int getDelta()
	{
		long currentTime = getTime();
		int delta = (int) (currentTime - lastFrame);
		lastFrame = getTime();
		return delta;
	}

	public static void updateFPS()
	{
		if (getTime() - lastFPS > 1000)
		{
			if (printFPS)
			{
				System.out.println("FPS: " + fps);
			}
			fps = 0;
			lastFPS += 1000;
		}
		fps++;
	}

	public static void main(String[] args)
	{
		try
		{
			if (fullscreen)
			{
				Display.setDisplayModeAndFullscreen(Display.getDesktopDisplayMode());
			} else
			{
				Display.setResizable(resizable);
				Display.setDisplayMode(new DisplayMode(800, 600));
			}
			Display.setTitle("Model Viewer");
			Display.setVSyncEnabled(vsync);
			Display.create();
		} catch (LWJGLException ex)
		{
			ex.printStackTrace();
			Display.destroy();
			System.exit(1);
		}

		if (fullscreen)
		{
			Mouse.setGrabbed(true);
		} else
		{
			Mouse.setGrabbed(false);
		}

		if (!GLContext.getCapabilities().OpenGL11)
		{
			System.err.println("Your OpenGL version doesn't support the required functionality.");
			Display.destroy();
			System.exit(1);
		}

		glMatrixMode(GL_PROJECTION);
		glLoadIdentity();
		gluPerspective(fov, (float) Display.getWidth() / (float) Display.getHeight(), zNear, zFar);
		glMatrixMode(GL_MODELVIEW);
		glLoadIdentity();

		glShadeModel(GL_SMOOTH);
		glEnable(GL_DEPTH_TEST);
		glEnable(GL_LIGHTING);
		glEnable(GL_LIGHT0);
		glLightModel(GL_LIGHT_MODEL_AMBIENT, asFloatBuffer(new float[] { 0.05f, 0.05f, 0.05f, 1f }));
		glLight(GL_LIGHT0, GL_DIFFUSE, asFloatBuffer(new float[] { 1.5f, 1.5f, 1.5f, 1f }));
		glEnable(GL_CULL_FACE);
		glCullFace(GL_BACK);
		glEnable(GL_COLOR_MATERIAL);
		glColorMaterial(GL_FRONT, GL_DIFFUSE);

		int objectDisplayList = glGenLists(1);
		glNewList(objectDisplayList, GL_COMPILE);
		{
			Model m = null;
			try
			{
				m = OBJLoader.loadModel(new File("src/hugebunny/bunny.obj"));
			} catch (FileNotFoundException e)
			{
				e.printStackTrace();
				Display.destroy();
				System.exit(1);
			} catch (IOException e)
			{
				e.printStackTrace();
				Display.destroy();
				System.exit(1);
			}
			glColor3f(0.4f, 0.27f, 0.17f);

			glBegin(GL_TRIANGLES);
			for (Face face : m.faces)
			{
				Vector3f n1 = m.normals.get((int) face.normal.x - 1);
				glNormal3f(n1.x, n1.y, n1.z);
				Vector3f v1 = m.vertices.get((int) face.vertex.x - 1);
				glVertex3f(v1.x, v1.y, v1.z);

				Vector3f n2 = m.normals.get((int) face.normal.y - 1);
				glNormal3f(n1.x, n1.y, n1.z);
				Vector3f v2 = m.vertices.get((int) face.vertex.y - 1);
				glVertex3f(v2.x, v2.y, v2.z);

				Vector3f n3 = m.normals.get((int) face.normal.z - 1);
				glNormal3f(n1.x, n1.y, n1.z);
				Vector3f v3 = m.vertices.get((int) face.vertex.z - 1);
				glVertex3f(v3.x, v3.y, v3.z);
			}
			glEnd();

			glColor3f(1, 1, 1);
		}
		glEndList();

		getDelta();
		lastFPS = getTime();

		while (running)
		{
			glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

			// glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

			int delta = getDelta();

			glCallList(objectDisplayList);

			glLoadIdentity();
			glRotatef(rotation.x, 1, 0, 0);
			glRotatef(rotation.y, 0, 1, 0);
			glRotatef(rotation.z, 0, 0, 1);
			glTranslatef(position.x, position.y, position.z);

			glLight(GL_LIGHT0, GL_POSITION, asFloatBuffer(new float[] { lightPosition[0], lightPosition[1], lightPosition[2], lightPosition[3] }));

			if (Mouse.isGrabbed())
			{
				float mouseDX = Mouse.getDX() * mouseSpeed * 0.16f;
				float mouseDY = Mouse.getDY() * mouseSpeed * 0.16f;
				if (rotation.y + mouseDX >= 360)
				{
					rotation.y = rotation.y + mouseDX - 360;
				} else if (rotation.y + mouseDX < 0)
				{
					rotation.y = 360 - rotation.y + mouseDX;
				} else
				{
					rotation.y += mouseDX;
				}
				if (rotation.x - mouseDY >= maxLookDown && rotation.x - mouseDY <= maxLookUp)
				{
					rotation.x += -mouseDY;
				} else if (rotation.x - mouseDY < maxLookDown)
				{
					rotation.x = maxLookDown;
				} else if (rotation.x - mouseDY > maxLookUp)
				{
					rotation.x = maxLookUp;
				}
			}

			// If you're looking for a challenge / something interesting, be
			// sure to have a look at this comment:
			// http://www.youtube.com/watch?v=OO_yNzAuDe4&lc=2e3e-Xz131-fklyBuY6e-xYiWWBv379j7BmQpZRysjc
			boolean keyUp = Keyboard.isKeyDown(Keyboard.KEY_UP) || Keyboard.isKeyDown(Keyboard.KEY_W);
			boolean keyDown = Keyboard.isKeyDown(Keyboard.KEY_DOWN) || Keyboard.isKeyDown(Keyboard.KEY_S);
			boolean keyLeft = Keyboard.isKeyDown(Keyboard.KEY_LEFT) || Keyboard.isKeyDown(Keyboard.KEY_A);
			boolean keyRight = Keyboard.isKeyDown(Keyboard.KEY_RIGHT) || Keyboard.isKeyDown(Keyboard.KEY_D);
			boolean flyUp = Keyboard.isKeyDown(Keyboard.KEY_SPACE);
			boolean flyDown = Keyboard.isKeyDown(Keyboard.KEY_LSHIFT);
			boolean moveFaster = Keyboard.isKeyDown(Keyboard.KEY_LCONTROL);
			boolean moveSlower = Keyboard.isKeyDown(Keyboard.KEY_TAB);

			if (moveFaster && !moveSlower)
			{
				walkingSpeed *= 4f;
			}
			if (moveSlower && !moveFaster)
			{
				walkingSpeed /= 10f;
			}

			if (keyUp && keyRight && !keyLeft && !keyDown)
			{
				float angle = rotation.y + 45;
				Vector3f newPosition = new Vector3f(position);
				float hypotenuse = (walkingSpeed * 0.0002f) * delta;
				float adjacent = hypotenuse * (float) Math.cos(Math.toRadians(angle));
				float opposite = (float) (Math.sin(Math.toRadians(angle)) * hypotenuse);
				newPosition.z += adjacent;
				newPosition.x -= opposite;
				position.z = newPosition.z;
				position.x = newPosition.x;
			}
			if (keyUp && keyLeft && !keyRight && !keyDown)
			{
				float angle = rotation.y - 45;
				Vector3f newPosition = new Vector3f(position);
				float hypotenuse = (walkingSpeed * 0.0002f) * delta;
				float adjacent = hypotenuse * (float) Math.cos(Math.toRadians(angle));
				float opposite = (float) (Math.sin(Math.toRadians(angle)) * hypotenuse);
				newPosition.z += adjacent;
				newPosition.x -= opposite;
				position.z = newPosition.z;
				position.x = newPosition.x;
			}
			if (keyUp && !keyLeft && !keyRight && !keyDown)
			{
				float angle = rotation.y;
				Vector3f newPosition = new Vector3f(position);
				float hypotenuse = (walkingSpeed * 0.0002f) * delta;
				float adjacent = hypotenuse * (float) Math.cos(Math.toRadians(angle));
				float opposite = (float) (Math.sin(Math.toRadians(angle)) * hypotenuse);
				newPosition.z += adjacent;
				newPosition.x -= opposite;
				position.z = newPosition.z;
				position.x = newPosition.x;
			}
			if (keyDown && keyLeft && !keyRight && !keyUp)
			{
				float angle = rotation.y - 135;
				Vector3f newPosition = new Vector3f(position);
				float hypotenuse = (walkingSpeed * 0.0002f) * delta;
				float adjacent = hypotenuse * (float) Math.cos(Math.toRadians(angle));
				float opposite = (float) (Math.sin(Math.toRadians(angle)) * hypotenuse);
				newPosition.z += adjacent;
				newPosition.x -= opposite;
				position.z = newPosition.z;
				position.x = newPosition.x;
			}
			if (keyDown && keyRight && !keyLeft && !keyUp)
			{
				float angle = rotation.y + 135;
				Vector3f newPosition = new Vector3f(position);
				float hypotenuse = (walkingSpeed * 0.0002f) * delta;
				float adjacent = hypotenuse * (float) Math.cos(Math.toRadians(angle));
				float opposite = (float) (Math.sin(Math.toRadians(angle)) * hypotenuse);
				newPosition.z += adjacent;
				newPosition.x -= opposite;
				position.z = newPosition.z;
				position.x = newPosition.x;
			}
			if (keyDown && !keyUp && !keyLeft && !keyRight)
			{
				float angle = rotation.y;
				Vector3f newPosition = new Vector3f(position);
				float hypotenuse = -(walkingSpeed * 0.0002f) * delta;
				float adjacent = hypotenuse * (float) Math.cos(Math.toRadians(angle));
				float opposite = (float) (Math.sin(Math.toRadians(angle)) * hypotenuse);
				newPosition.z += adjacent;
				newPosition.x -= opposite;
				position.z = newPosition.z;
				position.x = newPosition.x;
			}
			if (keyLeft && !keyRight && !keyUp && !keyDown)
			{
				float angle = rotation.y - 90;
				Vector3f newPosition = new Vector3f(position);
				float hypotenuse = (walkingSpeed * 0.0002f) * delta;
				float adjacent = hypotenuse * (float) Math.cos(Math.toRadians(angle));
				float opposite = (float) (Math.sin(Math.toRadians(angle)) * hypotenuse);
				newPosition.z += adjacent;
				newPosition.x -= opposite;
				position.z = newPosition.z;
				position.x = newPosition.x;
			}
			if (keyRight && !keyLeft && !keyUp && !keyDown)
			{
				float angle = rotation.y + 90;
				Vector3f newPosition = new Vector3f(position);
				float hypotenuse = (walkingSpeed * 0.0002f) * delta;
				float adjacent = hypotenuse * (float) Math.cos(Math.toRadians(angle));
				float opposite = (float) (Math.sin(Math.toRadians(angle)) * hypotenuse);
				newPosition.z += adjacent;
				newPosition.x -= opposite;
				position.z = newPosition.z;
				position.x = newPosition.x;
			}
			if (flyUp && !flyDown)
			{
				double newPositionY = (walkingSpeed * 0.0002) * delta;
				position.y -= newPositionY;
			}
			if (flyDown && !flyUp)
			{
				double newPositionY = (walkingSpeed * 0.0002) * delta;
				position.y += newPositionY;
			}
			if (moveFaster && !moveSlower)
			{
				walkingSpeed /= 4f;
			}
			if (moveSlower && !moveFaster)
			{
				walkingSpeed *= 10f;
			}
			while (Mouse.next())
			{
				if (Mouse.isButtonDown(0))
				{
					Mouse.setGrabbed(true);
				}
				if (Mouse.isButtonDown(1))
				{
					Mouse.setGrabbed(false);
				}

			}
			while (Keyboard.next())
			{
				if (Keyboard.isKeyDown(Keyboard.KEY_G))
				{
					lightPosition = new float[] { -position.x, -position.y, -position.z, 1f };
				}
				if (Keyboard.isKeyDown(Keyboard.KEY_C))
				{
					position = new Vector3f(0, 0, 0);
					rotation = new Vector3f(0, 0, 0);
				}
				if (Keyboard.isKeyDown(Keyboard.KEY_O))
				{
					mouseSpeed += 1;
					System.out.println("Mouse speed changed to " + mouseSpeed + ".");
				}
				if (Keyboard.isKeyDown(Keyboard.KEY_L))
				{
					if (mouseSpeed - 1 > 0)
					{
						mouseSpeed -= 1;
						System.out.println("Mouse speed changed to " + mouseSpeed + ".");
					}
				}
				if (Keyboard.isKeyDown(Keyboard.KEY_Q))
				{
					System.out.println("Walking speed changed to " + walkingSpeed + ".");
					walkingSpeed += 1;
				}
				if (Keyboard.isKeyDown(Keyboard.KEY_Z))
				{
					System.out.println("Walking speed changed to " + walkingSpeed + ".");
					walkingSpeed -= 1;
				}
				if (Keyboard.isKeyDown(Keyboard.KEY_F11))
				{
					try
					{
						Display.setFullscreen(!Display.isFullscreen());
						if (!Display.isFullscreen())
						{
							Display.setResizable(resizable);
							Display.setDisplayMode(new DisplayMode(800, 600));
							glViewport(0, 0, Display.getWidth(), Display.getHeight());
							glMatrixMode(GL_PROJECTION);
							glLoadIdentity();
							gluPerspective(fov, (float) Display.getWidth() / (float) Display.getHeight(), zNear, zFar);
							glMatrixMode(GL_MODELVIEW);
							glLoadIdentity();
						} else
						{
							glViewport(0, 0, Display.getWidth(), Display.getHeight());
							glMatrixMode(GL_PROJECTION);
							glLoadIdentity();
							gluPerspective(fov, (float) Display.getWidth() / (float) Display.getHeight(), zNear, zFar);
							glMatrixMode(GL_MODELVIEW);
							glLoadIdentity();
						}
					} catch (LWJGLException ex)
					{
						Logger.getLogger(ModelDemo.class.getName()).log(Level.SEVERE, null, ex);
					}
				}
				if (Keyboard.isKeyDown(Keyboard.KEY_ESCAPE))
				{
					if (!Mouse.isGrabbed() || Display.isFullscreen())
					{
						running = false;
					} else
					{
						Mouse.setGrabbed(false);
					}
				}
			}
			if (resizable)
			{
				if (Display.wasResized())
				{
					glViewport(0, 0, Display.getWidth(), Display.getHeight());
					glMatrixMode(GL_PROJECTION);
					glLoadIdentity();
					gluPerspective(fov, (float) Display.getWidth() / (float) Display.getHeight(), zNear, zFar);
					glMatrixMode(GL_MODELVIEW);
					glLoadIdentity();
				}
			}
			if (printFPS)
			{
				updateFPS();
			}
			Display.update();
			if (vsync)
			{
				Display.sync(60);
			}
			if (Display.isCloseRequested())
			{
				running = false;
			}
		}
		glDeleteLists(objectDisplayList, 1);
		Display.destroy();
		System.exit(0);
	}

	private static FloatBuffer asFloatBuffer(float[] values)
	{
		FloatBuffer buffer = BufferUtils.createFloatBuffer(values.length);
		buffer.put(values);
		buffer.flip();
		return buffer;
	}
}